/*
 * MIT License
 *
 * Copyright (c) 2024-2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include "include/exceptions.h"

#include "include/napi_utils.h"

namespace utils
{
napi_value GetUndefined(napi_env env)
{
    napi_value undefined;
    if (napi_get_undefined(env, &undefined) != napi_ok)
    {
        return nullptr;
    }
    return undefined;
};
napi_value GetGlobal(napi_env env)
{
    napi_value global;
    if (napi_get_global(env, &global) != napi_ok)
    {
        return nullptr;
    }
    return global;
};

napi_value GetProperty(napi_env env, napi_value object, napi_value key)
{
    bool has = false;
    if (napi_has_property(env, object, key, &has) != napi_ok)
        return nullptr;
    if (!has)
        return nullptr;
    napi_value result = nullptr;
    if (napi_get_property(env, object, key, &result) != napi_ok)
        return nullptr;
    return result;
};

napi_value GetProperty(napi_env env, napi_value object, const std::string &key)
{
    napi_value jskey = nullptr;
    napi_create_string_utf8(env, key.c_str(), key.length(), &jskey);
    return GetProperty(env, object, jskey);
};
napi_value GetProperty(napi_env env, napi_value object, const char *key)
{
    napi_value jskey = nullptr;
    napi_create_string_utf8(env, key, NAPI_AUTO_LENGTH, &jskey);
    return GetProperty(env, object, jskey);
};

bool HasProperty(napi_env env, napi_value object, napi_value key)
{
    bool has = false;
    if (napi_has_property(env, object, key, &has) != napi_ok)
        return false;
    return has;
};

bool HasProperty(napi_env env, napi_value object, const std::string &key)
{
    NapiEscapableHandleScope scope(env);
    napi_value jskey = nullptr;
    napi_create_string_utf8(env, key.c_str(), key.length(), &jskey);
    return HasProperty(env, object, jskey);
};
bool HasProperty(napi_env env, napi_value object, const char *key)
{
    NapiEscapableHandleScope scope(env);
    napi_value jskey = nullptr;
    napi_create_string_utf8(env, key, NAPI_AUTO_LENGTH, &jskey);
    return HasProperty(env, object, jskey);
};

napi_value ToString(napi_env env, napi_value value)
{
    napi_value toStringFunc = GetProperty(env, value, "toString");
    napi_value result = nullptr;
    if (toStringFunc != nullptr)
    {
        if (napi_call_function(env, value, toStringFunc, 0, nullptr, &result) != napi_ok)
        {
            return nullptr;
        }
        return result;
    }
    return CoerceToString(env, value);
}

void ThrowInvalidArg(napi_env env, const char *msg)
{
    std::string code = exceptions::code::INVALID_ARG_TYPE;
    std::string message = "[" + code + "]: " + msg;
    napi_throw_type_error(env, code.c_str(), message.c_str());
}

GlobalSymbol::GlobalSymbol(napi_env env, std::string name) : env_(env)
{
    napi_value desc = utils::CreateStringUtf8(env, name.c_str());
    napi_value symbol;
    napi_create_symbol(env, desc, &symbol);
    napi_create_reference(env, symbol, 1, &ref_);
    napi_add_env_cleanup_hook(env, CleanUp, reinterpret_cast<void *>(this));
}

GlobalSymbol::~GlobalSymbol()
{
    napi_delete_reference(env_, ref_);
}

napi_value GlobalSymbol::GetValue()
{
    napi_value value = nullptr;
    napi_get_reference_value(env_, ref_, &value);
    return value;
}

void GlobalSymbol::CleanUp(void *arg)
{
    delete reinterpret_cast<GlobalSymbol *>(arg);
}

napi_value GetWrapKey(napi_env env)
{
    thread_local GlobalSymbol *wrapKey = new GlobalSymbol(env, "runner:wrapper");
    return wrapKey->GetValue();
}

bool Wrap(napi_env env, napi_value object, void *data, napi_finalize finalize, void *hint)
{
    if (!data || !env || !object)
        return false;
    auto type = utils::TypeOf(env, object);
    if (type != napi_object && type != napi_function)
        return false;
    napi_value wrapperKey = GetWrapKey(env);
    if (HasProperty(env, object, wrapperKey))
        return false;

    WrapData *wrapData = new WrapData;
    wrapData->runFinalize_ = true;
    wrapData->data_ = data;
    wrapData->finalize_ = finalize;

    void *voidData = reinterpret_cast<void *>(wrapData);
    napi_value wrapper = nullptr;
    if (napi_create_external(env, voidData, nullptr, nullptr, &wrapper) != napi_ok)
        return false;
    napi_finalize secondFinalize = [](napi_env env, void *data, void *hint) {
        WrapData *that = reinterpret_cast<WrapData *>(data);
        if (that->runFinalize_ && that->finalize_)
            that->finalize_(env, that->data_, hint);
        delete that;
    };
    napi_set_property(env, object, wrapperKey, wrapper);
    if (napi_add_finalizer(env, object, voidData, secondFinalize, hint, nullptr) != napi_ok)
    {
        delete wrapData;
        return false;
    }
    return true;
}

void *Unwrap(napi_env env, napi_value object)
{
    if (!env || !object)
        return nullptr;
    auto type = utils::TypeOf(env, object);
    if (type != napi_object && type != napi_function)
        return nullptr;
    napi_value wrapper = utils::GetProperty(env, object, GetWrapKey(env));
    if (!wrapper)
        return nullptr;
    if (!IsExternal(env, wrapper))
        return nullptr;
    WrapData *data = nullptr;
    if (napi_get_value_external(env, wrapper, reinterpret_cast<void **>(&data)) != napi_ok)
        return nullptr;
    return data->data_;
}

void *RemoveWrap(napi_env env, napi_value object)
{
    if (!env || !object)
        return nullptr;
    auto type = utils::TypeOf(env, object);
    if (type != napi_object && type != napi_function)
        return nullptr;
    napi_value wrapper = utils::GetProperty(env, object, GetWrapKey(env));
    if (!wrapper)
        return nullptr;
    if (!IsExternal(env, wrapper))
        return nullptr;
    WrapData *data = nullptr;
    if (napi_get_value_external(env, wrapper, reinterpret_cast<void **>(&data)) != napi_ok)
        return nullptr;
    data->runFinalize_ = false;
    napi_delete_property(env, object, GetWrapKey(env), nullptr);
    // data will delete after finalize
    return data->data_;
}

bool ThrowIfTypeError(napi_env env, const char *msg, const char *argName, napi_value arg, napi_valuetype type)
{
    napi_valuetype valueType = utils::TypeOf(env, arg);
    if (valueType == type)
        return true;

    std::string code = exceptions::code::INVALID_ARG_TYPE;
    std::string message = "[" + code + "]: " + msg + "argument " + argName + " must be type of " +
                          ReadNapiValuetype(valueType) + " but " + ReadNapiValuetype(type) + " is provided.";
    napi_throw_type_error(env, exceptions::code::INVALID_ARG_TYPE, message.c_str());
    return false;
}

void SetUnenumerableProperty(napi_env env, napi_value object, const char *name, napi_value value)
{
    napi_property_descriptor desc[] = {{name, nullptr, nullptr, nullptr, nullptr, value, napi_default, nullptr}};
    napi_define_properties(env, object, 1, desc);
}

uv_loop_s* GetUVLoop(napi_env env)
{
    uv_loop_s* loop = nullptr;
    if (napi_get_uv_event_loop(env, &loop) != napi_ok)
        return nullptr;
    return loop;
}
} // namespace utils